package com.ost.kautilya.modeling.weka;

import java.sql.Types;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.logging.Logger;

import weka.core.Attribute;
import weka.core.Capabilities;
import weka.core.DenseInstance;
import weka.core.Instance;
import weka.core.Instances;
import weka.core.Utils;

import com.ost.kautilya.modeling.AttributeInfo;
import com.ost.kautilya.modeling.DataRow;
import com.ost.kautilya.modeling.Model;
import com.ost.kautilya.modeling.ModelConfiguration;
import com.ost.kautilya.modeling.ModelConfigurationParameter;
import com.ost.kautilya.modeling.ModelHandler;

public abstract class WekaModel extends Model {

	private final static Logger logger = Logger.getLogger(WekaModel.class.getName());

	private static final long serialVersionUID = 1L;
	private ArrayList<Attribute> atts;
	private int cli;

	private transient Instances mins;
	private transient Instance in;

	@Override
	public void cleanupSingletonResources() {
		super.cleanupSingletonResources();
		if (in != null) {
			in = null;
		}
		if (mins != null) {
			mins = null;
		}
	}

	@Override
	public void initTransients() {
		super.initTransients();
	}

	public Instances getInstances() {
		return mins;
	}

	public Instance getLiveInstance() {
		return in;
	}

	protected WekaModel(ModelType type) throws Exception {
		super(type);
	}

	public int getAttributeIndex(Attribute tatt) {
		return atts.indexOf(tatt);
	}

	public ArrayList<Attribute> getWekaAttributes() {
		return atts;
	}

	public Attribute getClassAttribute() {
		return (Attribute) atts.get(cli);
	}

	public void setAttributes(ArrayList<Attribute> wAtt) {
		atts = wAtt;
	}

	public void setClassIndex(int wCla) {
		cli = wCla;
	}

	public int getClassIndexInAttributes() {
		return cli;
	}

	@Override
	public void processNotAccountedRow(DataRow tr) throws Exception {

	}

	private final transient static SimpleDateFormat df = AttributeInfo.MODELER_DATE_FORMAT;
	protected transient SimpleDateFormat ddf = null;

	private static final String NULL_NOMINAL = "?";

	protected Instance updateInstance(DataRow rs, Instance in) {
		ModelHandler rpt = getHandler();
		for (int i = 0; i < getClasses().length + getAttributes().length; i++) {
			int index = i >= getClasses().length ? i - getClasses().length : i;
			AttributeInfo fld = rpt.getAttributeInfoByUID((i >= getClasses().length ? getAttributes() : getClasses())[index]);
			Attribute att = (Attribute) getWekaAttributes().get(i);
			try {
				switch (att.type()) {
				case Attribute.NOMINAL:
					String val = rs.getStringArray()[fld.getDataRowIndex()];
					in.setValue(att, val == null ? NULL_NOMINAL : val);
					break;
				case Attribute.NUMERIC:
					in.setValue(att, Double.valueOf(rs.getStringArray()[fld.getDataRowIndex()]));
					break;
				case Attribute.DATE:
					double value = 0;
					int fi = fld.getDataRowIndex();
					switch (rs.getJDBCDataType(fi)) {
					case Types.TIMESTAMP:
						value = rs.getTimeStampArray()[fi].getTime();
						break;
					case Types.TIME:
						value = rs.getTimeArray()[fi].getTime();
						break;
					case Types.DATE:
						value = rs.getDateArray()[fi].getTime();
						break;
					default:
						if (rs.isInjectedRow()) {
							try {
								value = ((Date) rs.getObjectArray()[fi]).getTime();
							} catch (NumberFormatException e) {
								value = Double.valueOf(rs.getStringArray()[fi]);
							} catch (ClassCastException e) {
								try {
									value = df.parse(rs.getStringArray()[fi]).getTime();
								} catch (Exception ex) {
									if (ddf == null) {
										ddf = new SimpleDateFormat("yyyy-MM-dd");
									}
									value = ddf.parse(rs.getStringArray()[fi]).getTime();
								}
							}
						} else {
							try {
								value = df.parse(rs.getStringArray()[fi]).getTime();
							} catch (Exception e) {
								if (ddf == null) {
									ddf = new SimpleDateFormat("yyyy-MM-dd");
								}
								value = ddf.parse(rs.getStringArray()[fi]).getTime();
							}
						}
						break;
					}
					in.setValue(att, value);
					break;
				}
			} catch (Exception e) {
				logger.severe(e.getMessage());
				continue;
			}
		}
		return in;
	}

	@Override
	public Object[] predictClasses(DataRow rs) throws Exception {
		Instance in = updateInstance(rs, this.in);
		in.setDataset(getInstances());
		return predictFor(in);
	}

	public abstract Object[] predictFor(Instance in) throws Exception;

	public abstract Capabilities getCapabilities();

	@Override
	public double[] getDistributionsForClasses(DataRow rs) throws Exception {
		Instance in = updateInstance(rs, this.in);
		return distributionFor(in);
	}

	public abstract double[] distributionFor(Instance in) throws Exception;

	public static ArrayList<Attribute> wekaAttributesFromReport(ModelHandler rpt, int[] classes, int[] attributes) throws Exception {
		ArrayList<Attribute> atts = new ArrayList<Attribute>();
		for (int i = 0; i < classes.length + attributes.length; i++) {
			int index = i >= classes.length ? i - classes.length : i;
			AttributeInfo fld = rpt.getAttributeInfoByUID((i >= classes.length ? attributes : classes)[index]);
			String name = fld.getAttributeTitle();
			Attribute att = null;
			if (fld.getAttributeDataType().equalsIgnoreCase("text")) {
				List<String> attVals = rpt.getAttributeDistinctValues(fld);
				att = new Attribute(name, attVals);
			} else if (fld.getAttributeDataType().equalsIgnoreCase("date")) {
				att = new Attribute(name, AttributeInfo.DEFAULT_DATE_FORMAT);
			} else {
				att = new Attribute(name);
			}
			atts.add(att);
		}
		return atts;
	}

	@Override
	public void prepare(ModelHandler rpt, ModelOperationMode mode) throws Exception {
		super.prepare(rpt, mode);
		setAttributes(wekaAttributesFromReport(rpt, getClasses(), getAttributes()));
		if (!(mode == ModelOperationMode.EVALUATION && mins != null)) {
			mins = (new Instances(getModelId(), getWekaAttributes(), 0));
			for (int i = 0; i < getClasses().length; i++) {
				mins.setClassIndex(i);
			}
		}
		if (mode == ModelOperationMode.TRAIN) {
			getBinary().put(TRAINING_SET, mins);
		}
		if (mode == ModelOperationMode.PREDICT) {
			in = (new DenseInstance(getWekaAttributes().size()));
			in.setDataset(mins);
			mins.add(in);
		}
		vins = new Instances(getModelId(), getWekaAttributes(), 0);
		for (int i = 0; i < getClasses().length; i++) {
			vins.setClassIndex(i);
		}
	}

	private transient Instances vins = null;

	@Override
	public void trainWithRecordIncrementally(DataRow tr) throws Exception {
		if (cli == 0) {
			// System.out.println("Adding to training set - " + tr);
			mins.add(dataRowToInstance(tr));
		}
	}

	private Instance dataRowToInstance(DataRow tr) {
		Instance in = new DenseInstance(getWekaAttributes().size());
		return updateInstance(tr, in);
	}

	abstract public void finalizeTraining(DataRow tr) throws Exception;

	public Instances getTrainingInstances() {
		return (Instances) getBinary().get(TRAINING_SET);
	}

	@Override
	public void evaluateRecordIncrementally(DataRow tr) throws Exception {
		if (cli == 0) {
			vins.add(dataRowToInstance(tr));
		}
	}

	public Instances getEvaluationInstances() {
		return vins;
	}

	@Override
	public String getBinaryName() {
		return super.getBinaryName();
	}

	@Override
	public void testForDataCompatability() throws Exception {
		ModelHandler handler = getHandler();
		int[] classes = getClasses();
		int[] attributes = getAttributes();
		for (int i = 0; i < classes.length + attributes.length; i++) {
			int index = i >= classes.length ? i - classes.length : i;
			AttributeInfo fld = handler.getAttributeInfoByUID((i >= classes.length ? attributes : classes)[index]);
			Capabilities cpb = getCapabilities();
			Attribute att = null;
			String name = fld.getAttributeTitle();
			if (fld.getAttributeDataType().equalsIgnoreCase("text")) {
				att = new Attribute(name, handler.getAttributeDistinctValues(fld));
			} else if (fld.getAttributeDataType().equalsIgnoreCase("date")) {
				att = new Attribute(name, fld.getAttributeDateFormat());
			} else
				att = new Attribute(name);
			cpb.testWithFail(att, i < classes.length);
		}
	}

	@Override
	public void applyConfiguration(ModelConfiguration config) throws Exception {
		String options = "";
		// System.out.println("Config - " + config);
		for (ModelConfigurationParameter param : config.keySet()) {
			String value = config.get(param).trim();
			if (param.isFlag() && !"true".equalsIgnoreCase(value))
				continue;
			if (param.isAvailable()) {
				options += ((!"".equals(param.getName()) ? " -" : "") + (param.getName()).trim());
				if (!param.isFlag() && !"".equalsIgnoreCase(value)) {
					options += " " + (value);
				}
			}
		}
		// System.out.println(options);
		applyConfiguration(Utils.splitOptions(options));
	}

	abstract public void applyConfiguration(String[] options) throws Exception;
}
